/* Mednafen - Multi-system Emulator
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "../psx.h"
#include "../frontio.h"
#include "guncon.h"

#include "../../mednafen-endian.h"

class InputDevice_GunCon : public InputDevice
{
   public:

      InputDevice_GunCon(void);
      virtual ~InputDevice_GunCon();

      virtual void Power(void);
      virtual int StateAction(StateMem* sm, int load, int data_only, const char* section_name);
      virtual void UpdateInput(const void *data);
      virtual bool RequireNoFrameskip(void);
      // GPULineHook modified to take upscale_factor for color detection (surf_pitchinpix unused)
      virtual int32_t GPULineHook(const int32_t line_timestamp, bool vsync, uint32 *pixels, const MDFN_PixelFormat* const format, const unsigned width, const unsigned pix_clock_offset, const unsigned pix_clock, const unsigned pix_clock_divider, const unsigned surf_pitchinpix, const unsigned upscale_factor);

      //
      //
      //
      virtual void SetDTR(bool new_dtr);
      virtual bool GetDSR(void);
      virtual bool Clock(bool TxD, int32 &dsr_pulse_delay);

   private:

      bool dtr;

      uint8 buttons;
      bool trigger_eff;
      bool trigger_noclear;
      uint16 hit_x, hit_y;

      int16 nom_x, nom_y;
      int32 os_shot_counter;
      bool prev_oss;

      int32 command_phase;
      uint32 bitpos;
      uint8 receive_buffer;

      uint8 command;

      uint8 transmit_buffer[16];
      uint32 transmit_pos;
      uint32 transmit_count;

      //
      // Video timing stuff
      bool prev_vsync;
      int line_counter;
};

InputDevice_GunCon::InputDevice_GunCon(void)
{
   Power();
}

InputDevice_GunCon::~InputDevice_GunCon()
{

}

void InputDevice_GunCon::Power(void)
{
   dtr = 0;

   buttons = 0;
   trigger_eff = 0;
   trigger_noclear = 0;
   hit_x = 0;
   hit_y = 0;

   nom_x = 0;
   nom_y = 0;

   os_shot_counter = 0;
   prev_oss = 0;

   command_phase = 0;

   bitpos = 0;

   receive_buffer = 0;

   command = 0;

   memset(transmit_buffer, 0, sizeof(transmit_buffer));

   transmit_pos = 0;
   transmit_count = 0;

   prev_vsync = 0;
   line_counter = 0;
}

int InputDevice_GunCon::StateAction(StateMem* sm, int load, int data_only, const char* section_name)
{
   SFORMAT StateRegs[] =
   {
      SFVAR(dtr),

      SFVAR(buttons),
      SFVAR(trigger_eff),
      SFVAR(trigger_noclear),
      SFVAR(hit_x),
      SFVAR(hit_y),

      SFVAR(nom_x),
      SFVAR(nom_y),
      SFVAR(os_shot_counter),
      SFVAR(prev_oss),

      SFVAR(command_phase),
      SFVAR(bitpos),
      SFVAR(receive_buffer),

      SFVAR(command),

      SFARRAY(transmit_buffer, sizeof(transmit_buffer)),
      SFVAR(transmit_pos),
      SFVAR(transmit_count),

      SFVAR(prev_vsync),
      SFVAR(line_counter),

      SFEND
   };
   int ret = MDFNSS_StateAction(sm, load, data_only, StateRegs, section_name);

   if(load)
   {
      if(((uint64_t)transmit_pos + transmit_count) > sizeof(transmit_buffer))
      {
         transmit_pos = 0;
         transmit_count = 0;
      }
   }

   return(ret);
}

void InputDevice_GunCon::UpdateInput(const void *data)
{
   uint8 *d8 = (uint8 *)data;

   nom_x = (int16)MDFN_de16lsb<false>(&d8[0]);
   nom_y = (int16)MDFN_de16lsb<false>(&d8[2]);

   trigger_noclear = (bool)(d8[4] & 0x1);
   trigger_eff |= trigger_noclear;

   buttons = d8[4] >> 1;

   if(os_shot_counter > 0)	// FIXME if UpdateInput() is ever called more than once per video frame(at ~50 or ~60Hz).
      os_shot_counter--;

   if((d8[4] & 0x8) && !prev_oss && os_shot_counter == 0)
      os_shot_counter = 4;
   prev_oss = d8[4] & 0x8;
}

bool InputDevice_GunCon::RequireNoFrameskip(void)
{
   return(true);
}

int32_t InputDevice_GunCon::GPULineHook(const int32_t line_timestamp, bool vsync, uint32 *pixels, const MDFN_PixelFormat* const format, const unsigned width,
      const unsigned pix_clock_offset, const unsigned pix_clock, const unsigned pix_clock_divider, const unsigned surf_pitchinpix, const unsigned upscale_factor)
{
   if(vsync && !prev_vsync)
      line_counter = 0;

   if(pixels && pix_clock)
   {
      const int avs = 16; // Not 16 for PAL, fixme.
      int32 gx;
      int32 gy;

      gx = (nom_x * 2 + pix_clock_divider) / (pix_clock_divider * 2);
      gy = nom_y;

      for(int32 ix = gx; ix < (gx + (int32)(pix_clock / 762925)); ix++)
      {
         if(ix >= 0 && ix < (int)width && line_counter >= (avs + gy) && line_counter < (avs + gy + 8))
         {
            int r, g, b, a;

            format->DecodeColor(pixels[ix * upscale_factor], r, g, b, a);

            if((r + g + b) >= 0x40)	// Wrong, but not COMPLETELY ABSOLUTELY wrong, at least. ;)
            {
               hit_x = (int64)(ix + pix_clock_offset) * 8000000 / pix_clock;	// GunCon has what appears to be an 8.00MHz ceramic resonator in it.
               hit_y = line_counter;
            }
         }
      }

      chair_x = gx;
      chair_y = (avs + gy) - line_counter;
   }

   line_counter++;

   return(PSX_EVENT_MAXTS);
}

void InputDevice_GunCon::SetDTR(bool new_dtr)
{
   if(!dtr && new_dtr)
   {
      command_phase = 0;
      bitpos = 0;
      transmit_pos = 0;
      transmit_count = 0;
   }
   else if(dtr && !new_dtr)
   {
      //if(bitpos || transmit_count)
      // printf("[PAD] Abort communication!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
   }

   dtr = new_dtr;
}

bool InputDevice_GunCon::GetDSR(void)
{
   if(!dtr)
      return(0);

   if(!bitpos && transmit_count)
      return(1);

   return(0);
}

bool InputDevice_GunCon::Clock(bool TxD, int32 &dsr_pulse_delay)
{
   bool ret = 1;

   dsr_pulse_delay = 0;

   if(!dtr)
      return(1);

   if(transmit_count)
      ret = (transmit_buffer[transmit_pos] >> bitpos) & 1;

   receive_buffer &= ~(1 << bitpos);
   receive_buffer |= TxD << bitpos;
   bitpos = (bitpos + 1) & 0x7;

   if(!bitpos)
   {
      //printf("[PAD] Receive: %02x -- command_phase=%d\n", receive_buffer, command_phase);

      if(transmit_count)
      {
         transmit_pos++;
         transmit_count--;
      }


      switch(command_phase)
      {
         case 0:
            if(receive_buffer != 0x01)
               command_phase = -1;
            else
            {
               transmit_buffer[0] = 0x63;
               transmit_pos = 0;
               transmit_count = 1;
               command_phase++;
            }
            break;

         case 2:
            //if(receive_buffer)
            // printf("%02x\n", receive_buffer);
            break;

         case 1:
            command = receive_buffer;
            command_phase++;

            transmit_buffer[0] = 0x5A;

            //puts("MOO");
            //if(command != 0x42)
            // fprintf(stderr, "GunCon unhandled command: 0x%02x\n", command);
            //assert(command == 0x42);
            if(command == 0x42)
            {
               transmit_buffer[1] = 0xFF ^ ((buttons & 0x01) << 3);
               transmit_buffer[2] = 0xFF ^ (trigger_eff << 5) ^ ((buttons & 0x02) << 5);

               if(os_shot_counter > 0)
               {
                  hit_x = 0x01;
                  hit_y = 0x0A;
                  transmit_buffer[2] |= (1 << 5);
                  if(os_shot_counter == 2 || os_shot_counter == 3)
                  {
                     transmit_buffer[2] &= ~(1 << 5);
                  }
               }

               MDFN_en16lsb<false>(&transmit_buffer[3], hit_x);
               MDFN_en16lsb<false>(&transmit_buffer[5], hit_y);

               hit_x = 0x01;
               hit_y = 0x0A;

               transmit_pos = 0;
               transmit_count = 7;

               trigger_eff = trigger_noclear;
            }
            else
            {
               command_phase = -1;
               transmit_buffer[1] = 0;
               transmit_buffer[2] = 0;
               transmit_pos = 0;
               transmit_count = 0;
            }
            break;

      }
   }

   if(!bitpos && transmit_count)
      dsr_pulse_delay = 100; //0x80; //0x40;

   return(ret);
}

InputDevice *Device_GunCon_Create(void)
{
   return new InputDevice_GunCon();
}


InputDeviceInputInfoStruct Device_GunCon_IDII[6] =
{
   { "x_axis", "X Axis", -1, IDIT_X_AXIS },
   { "y_axis", "Y Axis", -1, IDIT_Y_AXIS },

   { "trigger", "Trigger", 0, IDIT_BUTTON, NULL  },

   { "a",	"A",		 1, IDIT_BUTTON,	NULL },
   { "b", "B",		 2, IDIT_BUTTON,	NULL },

   { "offscreen_shot", "Offscreen Shot(Simulated)", 3, IDIT_BUTTON, NULL },	// Useful for "Judge Dredd", and probably not much else.
};
